# Copyright (C) 2006, 2007  Stephane Alnet
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# 

#
# For more information visit http://carrierclass.net
#
use strict; use warnings;

package CCNCore::Provision::CCNDatabase;
use base qw(CCNCore::Provision::Database);


# ----- Updating the schema while preserving the data -----

sub overwrite($)
{
    my $self = shift;
    my $c    = shift;
    
    for my $DBNAME ($self->get_db_names())
    {
        $self->database_done($DBNAME);
    }

    $self->log("overwrite(): nothing to do\n");
}

sub update_schema($)
{
    my $self = shift;
    my $c    = shift;

    my $bootstrap_file = CCNCore::Home::config.'/local/bin/model-'.$self->model_name().'-update.sql';
    print_to $c->root().$bootstrap_file, $self->bootstrap_sql(1,0);
    $c->commit_file($bootstrap_file);

    my $update_file_no_inserts = CCNCore::Home::config.'/local/bin/model-'.$self->model_name().'-update.no_inserts.sql';
    print_to $c->root().$update_file_no_inserts, $self->bootstrap_sql(1,1);
    $c->commit_file($update_file_no_inserts);

    my @DB_NAMES = $self->get_db_names();

    for my $DB_NAME (@DB_NAMES)
    {
        $self->log(ref($self)."::update_schema dbname=$DB_NAME\n");

        my $database_exists = __database_exists($DB_NAME);
        my $database_looksgood = $self->database_looksgood($DB_NAME);

        # Create the database if it does not exist (this happens when we create new clusters, etc.)
        my $createdb_file = CCNCore::Home::config().'/local/bin/model-'.$self->model_name().'-'.$DB_NAME.'-createdb.sql';
        print_to $c->root().$createdb_file, qq(CREATE DATABASE IF NOT EXISTS "$DB_NAME"\n);
        $c->commit_file($createdb_file);

        CCN::Scripting::run_command(CCN::Scripting::sql::SQL_FILE('mysql', $createdb_file));
        $self->{changed}->{$DB_NAME} = 1 if not $database_exists or not $database_looksgood or $c->changed($createdb_file);

        # Insert / copy the content
        $self->do_update($DB_NAME,$bootstrap_file,$update_file_no_inserts,$database_exists,$database_looksgood)
            if not $database_exists or not $database_looksgood 
            or $c->changed($bootstrap_file) or $c->changed($update_file_no_inserts) or $c->changed($createdb_file);
    }
}

sub has_changed
{
    my $self = shift;
    my $dbname = shift;

    if(defined $dbname)
    {
        return exists $self->{changed} and exists $self->{changed}->{$dbname};
    }
    else
    {
        return exists $self->{changed};
    }
}

# Actually processes the changes, if any

sub do_update($$$$$)
{
    my $self = shift;
    my $dbname = shift;
    my $file = shift;
    my $file_no_inserts = shift;
    my $database_exists = shift;
    my $database_looksgood = shift;

    $self->log(ref($self)."::do_update dbname=$dbname file=$file db_exists=$database_exists db_looksgood=$database_looksgood\n");

    $self->{changed}->{$dbname} = 1;
    
    # Clear the ccn_ok_* table to mark the tables as dirty.
    # This will get re-set by reprovision() and/or overwrite().
    $self->database_cleargood($dbname);

    # Dump the current content if any, then apply the changes, then re-populate the content.
    if($database_exists and $database_looksgood)
    {
        $self->dump($dbname);
        CCN::Scripting::run_command(CCN::Scripting::sql::SQL_FILE(${dbname}, $file_no_inserts));
        $self->undump($dbname);
    }
    # Otherwise create a new instance of the database
    else
    {
        CCN::Scripting::run_command(CCN::Scripting::sql::SQL_FILE(${dbname}, $file));
    }
}

# ----- Bootstrap helpers -----

sub bootstrap($)
{
    my $self = shift;
    my $c    = shift;

    my @DB_NAMES = $self->get_db_names();
    return unless @DB_NAMES;

    my $bootstrap_file = CCNCore::Home::config().'/local/bin/model-'.$self->model_name().'-bootstrap.sql';
    print_to $bootstrap_file, $self->bootstrap_sql(1,0);

    for my $DB_NAME (@DB_NAMES)
    {
        CCN::Scripting::run_command(CCN::Scripting::sql::SQL_FILE(${DB_NAME},$bootstrap_file));
        
        $self->database_done($DB_NAME);
    }
}

sub bootstrap_env()
{
    return {};
}

sub bootstrap_sql($$)
{
    my $self = shift;
    my $db_type = shift;
    my $no_inserts = shift;

    my $env = $self->bootstrap_env();
    if($db_type == 0)
    {
        # SQLite
        $env->{IFEXISTS}        = '';
        $env->{OPTIONS}         = '';
        $env->{OPTIONS_UTF8}    = '';
        $env->{AUTOINCREMENT}   = ''; # Not when doing a copy
        $env->{UNSIGNED}        = '';
        $env->{STAMP}           = '';
        $env->{MYSQL}           = '-- ';
    }
    if($db_type == 1)
    {
        # MySQL
        $env->{IFEXISTS}        = 'IF EXISTS';
        $env->{OPTIONS}         = 'DEFAULT CHARSET=latin1';
        $env->{OPTIONS_UTF8}    = 'DEFAULT CHARSET=utf8';
        $env->{AUTOINCREMENT}   = 'auto_increment';
        $env->{UNSIGNED}        = 'unsigned';
        $env->{STAMP}           = 'default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP';
        $env->{MYSQL}           = '';
    }

    my $drop_sql = content_of($self->dropmodel_file());
    my $sql      = content_of($self->model_file());

    $self->die ("Invalid dropmodel_file ".$self->dropmodel_file()) if not defined $drop_sql;
    $self->die ("Invalid model_file ".$self->model_file()) if not defined $sql;

    $drop_sql = template_text($drop_sql,$env);
    $sql      = template_text($sql,$env);
    $sql      = rewrite_mysql($sql) if $db_type == 1;
    $sql      =~ s/^\s*INSERT.*$//gm if $no_inserts;

    return $drop_sql . $sql;
}

sub init_sql($$)
{
    my $self = shift;
    my $db_type = shift;
    my $no_inserts = shift;

    my $env = $self->bootstrap_env();
    if($db_type == 0)
    {
        # SQLite
        $env->{IFEXISTS}        = '';
        $env->{OPTIONS}         = '';
        $env->{OPTIONS_UTF8}    = '';
        $env->{AUTOINCREMENT}   = ''; # Not when doing a copy
        $env->{UNSIGNED}        = '';
        $env->{STAMP}           = '';
        $env->{MYSQL}           = '-- ';
    }
    if($db_type == 1)
    {
        # MySQL
        $env->{IFEXISTS}        = 'IF EXISTS';
        $env->{OPTIONS}         = 'DEFAULT CHARSET=latin1';
        $env->{OPTIONS_UTF8}    = 'DEFAULT CHARSET=utf8';
        $env->{AUTOINCREMENT}   = 'auto_increment';
        $env->{UNSIGNED}        = 'unsigned';
        $env->{STAMP}           = 'default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP';
        $env->{MYSQL}           = '';
    }
    
    my $sql      = content_of($self->model_file());
    $self->die ("Invalid model_file ".$self->model_file()) if not defined $sql;
    
    $sql    = template_text($sql,$env);
    $sql    = rewrite_mysql($sql) if $db_type == 1;
    $sql    =~ s/^\s*INSERT.*$//gm if $no_inserts;
    
    return $sql;
}

# Where is the model file (CCN SQL formatted) for the module?
sub model_file()
{
    my $self = shift;
    return CCNCore::Home::manager.'/lib/sql/'.$self->model_name().'.sql';
}

# And the drop SQL file for the module?
sub dropmodel_file()
{
    my $self = shift;
    return CCNCore::Home::manager.'/lib/sql/'.$self->model_name().'-drop.sql';
}

use CCN::sql_copy::CCN;

# Dump the live database into a sqlite3 copy.
sub dump($)
{
    my $self = shift;
    my $db_name = shift;

    $self->log(ref($self)."::dump($db_name)\n");

    my $db_from = CCN::sql_copy::local_mysql($db_name);
    my $db_to = $self->initialize_sqlite3_dump($temp_file);

    $self->log(ref($self)."::dump($db_name): copy\n");
    return CCN::sql_copy::copy($db_from,$db_to,$self->model_file,undef,undef);
}

sub initialize_sqlite3_dump($)
{
    my $self = shift;

    my $temp_file = $self->dump_file().'.init';
    print_to $temp_file, $self->init_sql(0,1);
    $self->log(ref($self)."::dump($db_name): initialize ".$self->dump_file()." from $temp_file\n");
    return CCN::sql_copy::CCN::initialize_sqlite3($self->dump_file(),$temp_file);   
}

# Populate the live database back from the sqlite3 copy.
sub undump($)
{
    my $self = shift;
    my $db_name = shift;

    $self->log(ref($self)."::undump($db_name)\n");

    my $db_from = CCN::sql_copy::local_sqlite3($self->dump_file());
    $self->log(ref($self)."::undump($db_name): initialize\n");
    my $db_to   = CCN::sql_copy::local_mysql($db_name);
    $self->log(ref($self)."::undump($db_name): copy\n");
    return CCN::sql_copy::CCN::copy($db_from,$db_to,$self->model_file,undef,undef);
}

# Recreate the table layout then populate the live database back from the sqlite3 copy.
# Used in startup_mysql::restore_dbs.
sub reprovision($)
{
    my $self = shift;
    my $db_name = shift;

    $self->log(ref($self)."::reprovision($db_name)\n");

    my $file = CCNCore::Home::config.'/local/bin/model-'.$self->model_name().'-reprovision.sql';
    print_to $file, $self->bootstrap_sql(1,1);
    CCN::Scripting::run_command(CCN::Scripting::sql::SQL_FILE($db_name, $file));
    $self->undump($db_name) if -e $self->dump_file();
    $self->database_done($db_name);
}

=pod

sub rewrite_mysql($)
{
    my $sql = shift;
    # MySQL doesn't have a native "inet" type;
    # see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_inet-aton
    # So if we wanted to do maths on IPs we could do:
    #   $system_sql =~ s/inet/INT UNSIGNED/g;
    # and insert INET functions all over the place.
    # However we don't do maths on these (except in Emergency code, which uses different tables),
    # so let's store IP addresses and masks in strings.
    $sql =~ s/inet/VARCHAR(16)/g;
    # Same for MAC addresses. Note that these are stored as 00:00:00:00:00:00 to avoid
    # automatic conversion to numeric format if only digits are present.
    $sql =~ s/macaddr/CHAR(17)/g;
    # MySQL doesn't have a real Boolean type (it's actually a tinyint), neither does SQLite3.
    # SQLite3 doesn't really care, but treats it as a string. So we'd better do the same with
    # MySQL. Note that the values are uppercase and should be uppercase in queries too (thereby
    # ressembling SQL keywords.)
    $sql =~ s/BOOLEAN/ENUM('FALSE','TRUE')/g;
    
    return $sql;
}
=cut


1;